package org.conditure.container;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.conditure.container.annotation.Component;
import org.conditure.container.annotation.Destructor;
import org.conditure.container.annotation.Initializer;
import org.conditure.container.annotation.Injected;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An annotation-scanning based application container.
 * 
 * @see Component
 * @see Injected
 * @see Initializer
 * @see Destructor
 * 
 * @author Elvir
 */
public class ApplicationContainer {
	private static final Logger logger = LoggerFactory.getLogger(ApplicationContainer.class);
	
	private Set<String> packagedToScan;
	
	private Map<String, ComponentDefinition> componentClassCache;
	private Map<String, ComponentDefinition> componentNameCache;
	
	private State currentState;
	private enum State {
		CREATED,
		STARTED,
		STOPPED
	}
	
	/**
	 * Creates an application container that scans the following packages for {@link Component}s.
	 * 
	 * @param packages
	 */
	public ApplicationContainer(String... packages) {
		packagedToScan = new HashSet<String>();
		for (String packageName : packages) {
			packagedToScan.add(packageName);
		}
		
		currentState = State.CREATED;
	}
	
	/**
	 * Starts the application container.
	 * 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 * @throws InvocationTargetException 
	 * @throws IllegalArgumentException 
	 */
	public void start() throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
		// make sure we're in a valid state
		if (currentState == State.STARTED) {
			logger.error("Please stop the ApplicationContainer before starting it again.");
			
			return;
		}
		
		// create caches
		componentClassCache = new ConcurrentHashMap<String, ComponentDefinition>();
		componentNameCache = new ConcurrentHashMap<String, ComponentDefinition>();
		
		if (packagedToScan.size() > 0) {
			scanPackages();
		}

		performDependencyInjection();
		
		currentState = State.STARTED;
	}
	
	/**
	 * Stops the application container.
	 * 
	 * @throws InstantiationException 
	 * @throws InvocationTargetException 
	 * @throws IllegalAccessException 
	 * @throws IllegalArgumentException 
	 */
	public void stop() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
		// make sure we're in a valid state
		switch (currentState) {
			case CREATED:
			case STOPPED:
				logger.error("Please start the ApplicationContainer before attempting to stop it.");
				
				return;
		}
		
		destroyComponents();

		currentState = State.STOPPED;
	}
	
	/**
	 * Gets a component by its name.
	 * 
	 * @param componentName
	 * @return
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	public Object getComponent(String componentName) throws InstantiationException, IllegalAccessException {
		Object component = null;
		
		ComponentDefinition definition = componentNameCache.get(componentName);
		
		if (definition != null) {
			component = definition.getObject();
		}
		
		return component;
	}
	
	/**
	 * Scans packages for components.
	 * 
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 */
	private void scanPackages() throws InstantiationException, IllegalAccessException {
		Reflections reflections = new Reflections(packagedToScan.toArray());
		
		// scan packages for classes
		for (Class<?> componentClass : reflections.getTypesAnnotatedWith(Component.class, true)) {
			String componentClassName = componentClass.getName();
			
			// make sure we don't have a component of the same type
			if (!componentClassCache.containsKey(componentClass.getName())) {
				Component componentAnnotation = componentClass.getAnnotation(Component.class);

				// make sure we don't have a component with the same name
				if (!componentNameCache.containsKey(componentAnnotation.value())) {
					Set<Field> injectedFields = new HashSet<Field>();
					Set<Method> initializerMethods = new HashSet<Method>();
					Set<Method> destructorMethods = new HashSet<Method>();
					
					for (Field field : componentClass.getDeclaredFields()) {
						Injected injected = field.getAnnotation(Injected.class);
						
						if (injected != null) {
							injectedFields.add(field);
						}
					}
					
					for (Method method : componentClass.getDeclaredMethods()) {
						Initializer initializer = method.getAnnotation(Initializer.class);
						Destructor destructor = method.getAnnotation(Destructor.class);
						
						if (initializer != null) {
							initializerMethods.add(method);
						}

						if (destructor != null) {
							destructorMethods.add(method);
						}
					}
					
					ComponentDefinition definition = new ComponentDefinition(componentClass, componentAnnotation.value(), componentAnnotation.type(), 
							injectedFields, initializerMethods, destructorMethods);
					
					componentClassCache.put(componentClassName, definition);
					componentNameCache.put(componentAnnotation.value(), definition);
				} else {
					logger.warn("Ignoring collided component name with class: '{}'", componentClass);
				}
			} else {
				logger.warn("Ignoring already scanned component with class: '{}'", componentClass);
			}
		}
	}
	
	/**
	 * Performs dependency injection on the components.
	 * 
	 * @throws IllegalAccessException 
	 * @throws IllegalArgumentException 
	 * @throws InstantiationException 
	 * @throws InvocationTargetException 
	 */
	private void performDependencyInjection() throws IllegalArgumentException, IllegalAccessException, InstantiationException {
		// go through each component and scan for injections
		for (ComponentDefinition componentDefinition : componentClassCache.values()) {
			// inject the resolved beans into the field
			if (componentDefinition.getInjectedFields() != null) {
				for (Field field : componentDefinition.getInjectedFields()) {
					Injected injectedField = field.getAnnotation(Injected.class);
					
					Object beanToInject = getComponent(injectedField.value());
					
					// make sure we have a definition
					if (beanToInject != null) {
						field.setAccessible(true);
						
						logger.debug("Injecting component '{}' to field '{}'.", beanToInject, componentDefinition.getComponentName() + "." + injectedField.value());
						
						field.set(componentDefinition.getObject(), beanToInject);
	
						field.setAccessible(false);
					} else {
						logger.warn("Ignoring injection of field: '{}', for: '{}'", field.getName(), componentDefinition.getComponentClass());
					}
				}
			}
		}
	}
	
	/**
	 * Initializes the components.
	 * 
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException 
	 */
	public void initializeComponents() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
		// go through each component and call the initializers
		for (ComponentDefinition componentDefinition : componentClassCache.values()) {
			// call post-injection initializers
			if (componentDefinition.getInitializerMethods() != null) {
				for (Method method : componentDefinition.getInitializerMethods()) {
					method.setAccessible(true);
					
					try {
						method.invoke(componentDefinition.getObject());
					} finally {
						method.setAccessible(false);
					}
				}
			}
		}
	}

	/**
	 * Destroys the components.
	 * 
	 * @throws IllegalArgumentException
	 * @throws IllegalAccessException
	 * @throws InvocationTargetException
	 * @throws InstantiationException 
	 */
	public void destroyComponents() throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
		// go through each component and call the initializers
		for (ComponentDefinition componentDefinition : componentClassCache.values()) {
			// call post-injection initializers
			if (componentDefinition.getDestructorMethods() != null) {
				for (Method method : componentDefinition.getDestructorMethods()) {
					method.setAccessible(true);
					
					try {
						method.invoke(componentDefinition.getObject());
					} finally {
						method.setAccessible(false);
					}
				}
			}
		}
	}
}
